﻿/*
This project and all of its code is/are Copyright (C) 2017  Nils Hunter Alving

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
using DynamicDependencyContainer.Attributes;
using DynamicDependencyContainer.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace DynamicDependencyContainer.Models
{
    public abstract class RegisteredTypeBase : IRegisteredType
    {
        /// <summary>
        /// The object that will be used to resolve dependencies when the Resolve() method is invoked
        /// </summary>
        protected IResolveDependencies DependencyResolver { get; private set; }
        /// <summary>
        /// The interface that was registered
        /// </summary>
        public Type RegisteredInterface { get; protected set; }
        /// <summary>
        /// The type that was registered to represent that interface
        /// </summary>
        protected Type ImplementingClass { get; set; }

        protected List<object> ConstructorArguments;
        protected ConstructorInfo Constructor;

        /// <summary>
        /// The constructor
        /// </summary>
        /// <param name="registeredType">The interface type that is being registered</param>
        /// <param name="dependencyResolver">The object that will be used to resolve dependencies when the Resolve() method is invoked</param>
        public RegisteredTypeBase(Type interfaceType, IResolveDependencies dependencyResolver)
        {
            DependencyResolver = dependencyResolver;
            RegisteredInterface = interfaceType;
        }

        /// <summary>
        /// Determines if this instance can be used to resolve a specified type
        /// </summary>
        /// <returns></returns>
        public virtual bool CanResolveType(Type type)
        {
            return RegisteredInterface == type;
        }

        protected virtual void Initialize(Type interfaceImplemented, Type implementingClass)
        {
            if (implementingClass.IsInterface)
            {
                throw new ArgumentException($"Type {implementingClass.Name} is an interface and cannot be instantiated");
            }
            RegisteredInterface = interfaceImplemented;
            ImplementingClass = implementingClass;
        }
        protected virtual void SetConstructor(Type[] paramTypes)
        {
            Constructor = ImplementingClass.GetConstructor(paramTypes);
            if (Constructor == null)
            {
                var argList = paramTypes == Type.EmptyTypes ? "none" : string.Join(", ", paramTypes.Select(a => a.Name));
                throw new ArgumentException($"Type {ImplementingClass.Name} does not have constructor that takes arguments of types: {argList}");
            }
        }

        public abstract object Resolve();
    }
    public class RegisteredType<T> : RegisteredTypeBase where T : class
    {
        protected virtual T Instance { get; set; }
        protected Action<T> InitializationFunction { get; private set; }

        private RegisteredType(IResolveDependencies dependencyResolver)
            : base(typeof(T), dependencyResolver)
        {

        }
        /// <summary>
        /// Registers the specified class as the type to be used to represent the specified interface
        /// </summary>
        /// <param name="interfaceImplemented">The interface that is being registered</param>
        /// <param name="implementingClass">The class that implements the interface</param>
        /// <param name="dependencyResolver">The object that will be used to resolve dependencies when the Resolve() method is invoked</param>
        /// <exception cref="ArgumentException"></exception>
        public RegisteredType(Type interfaceImplemented, Type implementingClass, IResolveDependencies dependencyResolver)
            :this(dependencyResolver)
        {
            Initialize(interfaceImplemented, implementingClass);
            SetConstructor(Type.EmptyTypes);
        }
        /// <summary>
        /// Registers the specified instance as the singleton to be returned when Resolve() is called. Note that does not prevent
        /// a caller from creating a new instance by calling one of the New() methods
        /// </summary>
        /// <param name="interfaceImplemented">The interface that is being registered</param>
        /// <param name="instance">An instance of the class that implements the interface</param>
        /// <param name="dependencyResolver">The object that will be used to resolve dependencies when the Resolve() method is invoked</param>
        public RegisteredType(Type interfaceImplemented, object instance, IResolveDependencies dependencyResolver)
            : this(dependencyResolver)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            Initialize(interfaceImplemented, instance.GetType());
            Instance = instance as T;
        }
        ///<summary>
        /// Registers the specified class as the type to be used to represent the specified interface, and
        /// specifies the arguments to be passed to the constructor when a new instance is created
        /// </summary>
        /// <param name="interfaceImplemented">The interface that is being registered</param>
        /// <param name="implementingClass">The class that implements the interface</param>
        /// <param name="constructorArguments">The arguments to be passed to the constructor when Resolve is called or a new instance is created</param>
        /// <param name="dependencyResolver">The object that will be used to resolve dependencies when the Resolve() method is invoked</param>
        /// <exception cref="ArgumentException"></exception>
        public RegisteredType(Type interfaceImplemented, Type implementingClass, object[] constructorArguments, IResolveDependencies dependencyResolver)
            : this(dependencyResolver)
        {
            if (constructorArguments == null)
            {
                throw new ArgumentNullException("constructorArguments");
            }
            Initialize(interfaceImplemented, implementingClass);

            ConstructorArguments = constructorArguments.ToList();
            SetConstructor(ConstructorArguments.Select(a =>
            {
                var type = a as Type; //TODO: improve readability
                return type != null ? type : a.GetType();
            }).ToArray());
        }

        /// <summary>
        /// Registers the specified class as the type to be used to represent the specified interface
        /// </summary>
        /// <param name="implementingClass">The class that implements the interface</param>
        /// <param name="initializerFunction">The function that will initialize the object each time it is resolved</param>
        /// <param name="dependencyResolver">The object that will be used to resolve dependencies when the Resolve() method is invoked</param>
        public RegisteredType(Type implementingClass, Action<T> initializerFunction, IResolveDependencies dependencyResolver)
            : this(dependencyResolver)
        {
            if (initializerFunction == null)
            {
                throw new ArgumentNullException("initialzerFunction");
            }
            Initialize(typeof(T), implementingClass, initializerFunction);
            SetConstructor(Type.EmptyTypes);
        }
        ///<summary>
        /// Registers the specified class as the type to be used to represent the specified interface, and
        /// specifies the arguments to be passed to the constructor when a new instance is created
        /// </summary>
        /// <param name="implementingClass">The class that implements the interface</param>
        /// <param name="initializationFunction">An intialization delegate that will be invoked after the object is constructed. 
        /// The object being constructed will be passed to the delegate as a parameter</param>
        /// <param name="constructorArguments">The arguments to be passed to the constructor when Resolve is called or a new instance is created</param>
        /// <param name="dependencyResolver">The object that will be used to resolve dependencies when the Resolve() method is invoked</param>
        /// <exception cref="ArgumentException"></exception>
        public RegisteredType(Type implementingClass, Action<T> initializationFunction, object[] constructorArguments, IResolveDependencies dependencyResolver)
            : this(dependencyResolver)
        {
            if (constructorArguments == null)
            {
                throw new ArgumentNullException("constructorArguments");
            }
            if (initializationFunction == null)
            {
                throw new ArgumentNullException("initialzerFunction");
            }
            Initialize(typeof(T), implementingClass, initializationFunction);
            ConstructorArguments = constructorArguments.ToList();
            SetConstructor(ConstructorArguments.Select(a => a.GetType()).ToArray());
        }
        protected override void Initialize(Type interfaceImplemented, Type implementingClass)
        {
            if (implementingClass.IsInterface)
            {
                throw new ArgumentException($"Type {implementingClass.Name} is an interface and cannot be instantiated");
            }
            RegisteredInterface = interfaceImplemented;
            ImplementingClass = implementingClass;
        }
        protected virtual void Initialize(Type interfaceImplemented, Type implementingClass, Action<T> initializationFunction)
        {
            Initialize(interfaceImplemented, implementingClass);
            InitializationFunction = initializationFunction;
        }
        protected override void SetConstructor(Type[] paramTypes)
        {
            Constructor = ImplementingClass.GetConstructor(paramTypes);
            if (Constructor == null)
            {
                var argList = paramTypes == Type.EmptyTypes ? "none" : string.Join(", ", paramTypes.Select(a => a.Name));
                throw new ArgumentException($"Type {ImplementingClass.Name} does not have constructor that takes arguments of types: {argList}");
            }
        }
        /// <summary>
        /// Returns either the registered instance or a new instance of the registered implementing class
        /// </summary>
        /// <exception cref="InvalidOperationException"></exception>
        /// <returns>An instance of the implementing class</returns>
        public override object Resolve()
        {
            if (Instance != null)
            {
                return Instance;
            }
            if (Constructor == null)
            {
                throw new Exception($"Attempt to resolve interface: {RegisteredInterface} with type: {ImplementingClass} cannot be processed. The instance is null and no constructor was specified when the instance was registered");
            }

            return CreateObject();
        }
        protected T CreateObject()
        {
            var instance = ConstructorArguments == null ? New() : New(ResolveConstructorArgs());
            InitializeDependencyProperties(instance);

            if (InitializationFunction == null)
            {
                return instance;
            }
            InitializationFunction(instance);
            return instance;
        }
        private void InitializeDependencyProperties(T instance)
        {
            var nonPublicDependencyProperties = instance.GetType()
                                                        .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
                                                        .Where(p => p.GetCustomAttribute(typeof(DependencyAttribute)) != null);

            var publicDependecyProperties = instance.GetType()
                                                    .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                                    .Where(p => p.GetCustomAttribute(typeof(DependencyAttribute)) != null);

            nonPublicDependencyProperties.Union(publicDependecyProperties)
                    .ToList()
                    .ForEach(dp =>
                    {
                        var injectedProperty = DependencyResolver.Resolve(dp.PropertyType);
                        dp.SetValue(instance, injectedProperty);
                    });
        }
        internal virtual object[] ResolveConstructorArgs()
        {
            var args = new List<object>(ConstructorArguments.Count);

            ConstructorArguments.ForEach(ca =>
            {
                object constructorArg = ca;

                var type = ca as Type;
                if (type != null)
                {
                    constructorArg = DependencyResolver.Resolve(type);
                }

                args.Add(constructorArg);
            });

            return args.ToArray();
        }
        /// <summary>
        /// Invokes the default constructor on the implementing class
        /// </summary>
        /// <exception cref="InvalidOperationException"></exception>
        /// <returns>An instance of the implementing class</returns>
        internal virtual T New()
        {
            try
            {
                return Activator.CreateInstance(ImplementingClass) as T;
            }
            catch (Exception e)
            {
                throw new InvalidOperationException($"Attempting to create an instance of type {ImplementingClass} with the default constructor threw exception: {e.GetType()}, {e.Message}");
            }
        }
        /// <summary>
        /// Invokes the constructor on the implementing class that has a matching list of parameters
        /// </summary>
        /// <param name="constructorArgs"></param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <returns>An instance of the implementing class</returns>
        internal virtual T New(object[] constructorArgs)
        {
            try
            {
                return Activator.CreateInstance(ImplementingClass, constructorArgs) as T;
            }
            catch (Exception e)
            {
                var argList = string.Join(", ", constructorArgs.Select(a => a.GetType().Name));
                throw new InvalidOperationException($"Attempting to create an instance of type {ImplementingClass} with arguments of type {argList} threw exception: {e.GetType()}, {e.Message}");
            }
        }
    }
}
